/*
 * Copyright (C) 2004, 2005, 2006 Joe Walnes.
 * Copyright (C) 2006, 2007, 2008, 2010, 2011, 2012, 2013, 2014, 2015, 2016 XStream Committers.
 * All rights reserved.
 *
 * The software in this package is published under the terms of the BSD
 * style license a copy of which has been included with this distribution in
 * the LICENSE.txt file.
 * 
 * Created on 21. December 2004 by Joe Walnes
 */
package com.feilong.lib.xstream.converters.reflection;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputValidation;
import java.io.ObjectStreamClass;
import java.io.ObjectStreamField;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.feilong.lib.xstream.converters.ConversionException;
import com.feilong.lib.xstream.converters.MarshallingContext;
import com.feilong.lib.xstream.converters.UnmarshallingContext;
import com.feilong.lib.xstream.core.ClassLoaderReference;
import com.feilong.lib.xstream.core.JVM;
import com.feilong.lib.xstream.core.util.CustomObjectInputStream;
import com.feilong.lib.xstream.core.util.CustomObjectOutputStream;
import com.feilong.lib.xstream.core.util.Fields;
import com.feilong.lib.xstream.core.util.HierarchicalStreams;
import com.feilong.lib.xstream.io.ExtendedHierarchicalStreamWriterHelper;
import com.feilong.lib.xstream.io.HierarchicalStreamReader;
import com.feilong.lib.xstream.io.HierarchicalStreamWriter;
import com.feilong.lib.xstream.io.StreamException;
import com.feilong.lib.xstream.mapper.Mapper;

/**
 * Emulates the mechanism used by standard Java Serialization for classes that implement java.io.Serializable AND
 * implement or inherit a custom readObject()/writeObject() method.
 *
 * <h3>Supported features of serialization</h3>
 * <ul>
 * <li>readObject(), writeObject()</li>
 * <li>class inheritance</li>
 * <li>readResolve(), writeReplace()</li>
 * <li>getFields(), putFields(), writeFields(), readFields()</li>
 * <li>ObjectStreamField[] serialPersistentFields</li>
 * <li>ObjectInputValidation</li>
 * </ul>
 *
 * @author Joe Walnes
 * @author J&ouml;rg Schaible
 */
public class SerializableConverter extends AbstractReflectionConverter{

    private static final String        ELEMENT_NULL                   = "null";

    private static final String        ELEMENT_DEFAULT                = "default";

    private static final String        ELEMENT_UNSERIALIZABLE_PARENTS = "unserializable-parents";

    private static final String        ATTRIBUTE_CLASS                = "class";

    private static final String        ATTRIBUTE_SERIALIZATION        = "serialization";

    private static final String        ATTRIBUTE_VALUE_CUSTOM         = "custom";

    private static final String        ELEMENT_FIELDS                 = "fields";

    private static final String        ELEMENT_FIELD                  = "field";

    private static final String        ATTRIBUTE_NAME                 = "name";

    private final ClassLoaderReference classLoaderReference;

    /**
     * Construct a SerializableConverter.
     *
     * @param mapper
     *            the mapper chain instance
     * @param reflectionProvider
     *            the reflection provider
     * @param classLoaderReference
     *            the reference to the {@link ClassLoader} of the XStream instance
     * @since 1.4.5
     */
    public SerializableConverter(Mapper mapper, ReflectionProvider reflectionProvider, ClassLoaderReference classLoaderReference){
        super(mapper, new UnserializableParentsReflectionProvider(reflectionProvider));
        this.classLoaderReference = classLoaderReference;
    }

    /**
     * @deprecated As of 1.4.5 use {@link #SerializableConverter(Mapper, ReflectionProvider, ClassLoaderReference)}
     */
    @Deprecated
    public SerializableConverter(Mapper mapper, ReflectionProvider reflectionProvider, ClassLoader classLoader){
        this(mapper, reflectionProvider, new ClassLoaderReference(classLoader));
    }

    /**
     * @deprecated As of 1.4 use {@link #SerializableConverter(Mapper, ReflectionProvider, ClassLoaderReference)}
     */
    @Deprecated
    public SerializableConverter(Mapper mapper, ReflectionProvider reflectionProvider){
        this(mapper, new UnserializableParentsReflectionProvider(reflectionProvider), new ClassLoaderReference(null));
    }

    @Override
    public boolean canConvert(Class type){
        return JVM.canCreateDerivedObjectOutputStream() && isSerializable(type);
    }

    private boolean isSerializable(Class type){
        if (type != null && Serializable.class.isAssignableFrom(type) && !type.isInterface()
                        && (serializationMembers.supportsReadObject(type, true) || serializationMembers.supportsWriteObject(type, true))){
            for (Iterator iter = hierarchyFor(type).iterator(); iter.hasNext();){
                if (!Serializable.class.isAssignableFrom((Class) iter.next())){
                    return canAccess(type);
                }
            }
            return true;
        }
        return false;
    }

    @Override
    public void doMarshal(final Object source,final HierarchicalStreamWriter writer,final MarshallingContext context){
        String attributeName = mapper.aliasForSystemAttribute(ATTRIBUTE_SERIALIZATION);
        if (attributeName != null){
            writer.addAttribute(attributeName, ATTRIBUTE_VALUE_CUSTOM);
        }

        // this is an array as it's a non final value that's accessed from an anonymous inner class.
        final Class[] currentType = new Class[1];
        final boolean[] writtenClassWrapper = { false };

        CustomObjectOutputStream.StreamCallback callback = new CustomObjectOutputStream.StreamCallback(){

            @Override
            public void writeToStream(Object object){
                if (object == null){
                    writer.startNode(ELEMENT_NULL);
                    writer.endNode();
                }else{
                    ExtendedHierarchicalStreamWriterHelper.startNode(writer, mapper.serializedClass(object.getClass()), object.getClass());
                    context.convertAnother(object);
                    writer.endNode();
                }
            }

            @Override
            public void writeFieldsToStream(Map fields){
                ObjectStreamClass objectStreamClass = ObjectStreamClass.lookup(currentType[0]);

                writer.startNode(ELEMENT_DEFAULT);
                for (Iterator iterator = fields.keySet().iterator(); iterator.hasNext();){
                    String name = (String) iterator.next();
                    if (!mapper.shouldSerializeMember(currentType[0], name)){
                        continue;
                    }
                    ObjectStreamField field = objectStreamClass.getField(name);
                    Object value = fields.get(name);
                    if (field == null){
                        throw new MissingFieldException(value.getClass().getName(), name);
                    }
                    if (value != null){
                        ExtendedHierarchicalStreamWriterHelper
                                        .startNode(writer, mapper.serializedMember(source.getClass(), name), value.getClass());
                        if (field.getType() != value.getClass() && !field.getType().isPrimitive()){
                            String attributeName = mapper.aliasForSystemAttribute(ATTRIBUTE_CLASS);
                            if (attributeName != null){
                                writer.addAttribute(attributeName, mapper.serializedClass(value.getClass()));
                            }
                        }
                        context.convertAnother(value);
                        writer.endNode();
                    }
                }
                writer.endNode();
            }

            @Override
            public void defaultWriteObject(){
                boolean writtenDefaultFields = false;

                ObjectStreamClass objectStreamClass = ObjectStreamClass.lookup(currentType[0]);

                if (objectStreamClass == null){
                    return;
                }

                ObjectStreamField[] fields = objectStreamClass.getFields();
                for (ObjectStreamField field : fields){
                    Object value = readField(field, currentType[0], source);
                    if (value != null){
                        if (!writtenClassWrapper[0]){
                            writer.startNode(mapper.serializedClass(currentType[0]));
                            writtenClassWrapper[0] = true;
                        }
                        if (!writtenDefaultFields){
                            writer.startNode(ELEMENT_DEFAULT);
                            writtenDefaultFields = true;
                        }
                        if (!mapper.shouldSerializeMember(currentType[0], field.getName())){
                            continue;
                        }

                        Class actualType = value.getClass();
                        ExtendedHierarchicalStreamWriterHelper
                                        .startNode(writer, mapper.serializedMember(source.getClass(), field.getName()), actualType);
                        Class defaultType = mapper.defaultImplementationOf(field.getType());
                        if (!actualType.equals(defaultType)){
                            String attributeName = mapper.aliasForSystemAttribute(ATTRIBUTE_CLASS);
                            if (attributeName != null){
                                writer.addAttribute(attributeName, mapper.serializedClass(actualType));
                            }
                        }

                        context.convertAnother(value);

                        writer.endNode();
                    }
                }
                if (writtenClassWrapper[0] && !writtenDefaultFields){
                    writer.startNode(ELEMENT_DEFAULT);
                    writer.endNode();
                }else if (writtenDefaultFields){
                    writer.endNode();
                }
            }

            @Override
            public void flush(){
                writer.flush();
            }

            @Override
            public void close(){
                throw new UnsupportedOperationException("Objects are not allowed to call ObjectOutputStream.close() from writeObject()");
            }
        };

        try{
            boolean mustHandleUnserializableParent = false;
            Iterator classHieararchy = hierarchyFor(source.getClass()).iterator();
            while (classHieararchy.hasNext()){
                currentType[0] = (Class) classHieararchy.next();
                if (!Serializable.class.isAssignableFrom(currentType[0])){
                    mustHandleUnserializableParent = true;
                    continue;
                }else{
                    if (mustHandleUnserializableParent){
                        marshalUnserializableParent(writer, context, source);
                        mustHandleUnserializableParent = false;
                    }
                    if (serializationMembers.supportsWriteObject(currentType[0], false)){
                        writtenClassWrapper[0] = true;
                        writer.startNode(mapper.serializedClass(currentType[0]));
                        if (currentType[0] != mapper.defaultImplementationOf(currentType[0])){
                            String classAttributeName = mapper.aliasForSystemAttribute(ATTRIBUTE_CLASS);
                            if (classAttributeName != null){
                                writer.addAttribute(classAttributeName, currentType[0].getName());
                            }
                        }
                        CustomObjectOutputStream objectOutputStream = CustomObjectOutputStream.getInstance(context, callback);
                        serializationMembers.callWriteObject(currentType[0], source, objectOutputStream);
                        objectOutputStream.popCallback();
                        writer.endNode();
                    }else if (serializationMembers.supportsReadObject(currentType[0], false)){
                        // Special case for objects that have readObject(), but not writeObject().
                        // The class wrapper is always written, whether or not this class in the hierarchy has
                        // serializable fields. This guarantees that readObject() will be called upon deserialization.
                        writtenClassWrapper[0] = true;
                        writer.startNode(mapper.serializedClass(currentType[0]));
                        if (currentType[0] != mapper.defaultImplementationOf(currentType[0])){
                            String classAttributeName = mapper.aliasForSystemAttribute(ATTRIBUTE_CLASS);
                            if (classAttributeName != null){
                                writer.addAttribute(classAttributeName, currentType[0].getName());
                            }
                        }
                        callback.defaultWriteObject();
                        writer.endNode();
                    }else{
                        writtenClassWrapper[0] = false;
                        callback.defaultWriteObject();
                        if (writtenClassWrapper[0]){
                            writer.endNode();
                        }
                    }
                }
            }
        }catch (IOException e){
            throw new StreamException("Cannot write defaults", e);
        }
    }

    protected void marshalUnserializableParent(
                    final HierarchicalStreamWriter writer,
                    final MarshallingContext context,
                    final Object replacedSource){
        writer.startNode(ELEMENT_UNSERIALIZABLE_PARENTS);
        super.doMarshal(replacedSource, writer, context);
        writer.endNode();
    }

    private Object readField(ObjectStreamField field,Class type,Object instance){
        Field javaField = Fields.find(type, field.getName());
        return Fields.read(javaField, instance);
    }

    protected List hierarchyFor(Class type){
        List result = new ArrayList();
        while (type != Object.class && type != null){
            result.add(type);
            type = type.getSuperclass();
        }

        // In Java Object Serialization, the classes are deserialized starting from parent class and moving down.
        Collections.reverse(result);

        return result;
    }

    @Override
    public Object doUnmarshal(final Object result,final HierarchicalStreamReader reader,final UnmarshallingContext context){
        // this is an array as it's a non final value that's accessed from an anonymous inner class.
        final Class[] currentType = new Class[1];

        String attributeName = mapper.aliasForSystemAttribute(ATTRIBUTE_SERIALIZATION);
        if (attributeName != null && !ATTRIBUTE_VALUE_CUSTOM.equals(reader.getAttribute(attributeName))){
            throw new ConversionException("Cannot deserialize object with new readObject()/writeObject() methods");
        }

        CustomObjectInputStream.StreamCallback callback = new CustomObjectInputStream.StreamCallback(){

            @Override
            public Object readFromStream(){
                reader.moveDown();
                Class type = HierarchicalStreams.readClassType(reader, mapper);
                Object value = context.convertAnother(result, type);
                reader.moveUp();
                return value;
            }

            @Override
            public Map readFieldsFromStream(){
                final Map fields = new HashMap();
                reader.moveDown();
                if (reader.getNodeName().equals(ELEMENT_FIELDS)){
                    // Maintain compatibility with XStream 1.1.0
                    while (reader.hasMoreChildren()){
                        reader.moveDown();
                        if (!reader.getNodeName().equals(ELEMENT_FIELD)){
                            throw new ConversionException("Expected <" + ELEMENT_FIELD + "/> element inside <" + ELEMENT_FIELD + "/>");
                        }
                        String name = reader.getAttribute(ATTRIBUTE_NAME);
                        Class type = mapper.realClass(reader.getAttribute(ATTRIBUTE_CLASS));
                        Object value = context.convertAnother(result, type);
                        fields.put(name, value);
                        reader.moveUp();
                    }
                }else if (reader.getNodeName().equals(ELEMENT_DEFAULT)){
                    // New format introduced in XStream 1.1.1
                    ObjectStreamClass objectStreamClass = ObjectStreamClass.lookup(currentType[0]);
                    while (reader.hasMoreChildren()){
                        reader.moveDown();
                        String name = mapper.realMember(currentType[0], reader.getNodeName());
                        if (mapper.shouldSerializeMember(currentType[0], name)){
                            String classAttribute = HierarchicalStreams.readClassAttribute(reader, mapper);
                            Class type;
                            if (classAttribute != null){
                                type = mapper.realClass(classAttribute);
                            }else{
                                ObjectStreamField field = objectStreamClass.getField(name);
                                if (field == null){
                                    throw new MissingFieldException(currentType[0].getName(), name);
                                }
                                type = field.getType();
                            }
                            Object value = context.convertAnother(result, type);
                            fields.put(name, value);
                        }
                        reader.moveUp();
                    }
                }else{
                    throw new ConversionException(
                                    "Expected <" + ELEMENT_FIELDS + "/> or <" + ELEMENT_DEFAULT
                                                    + "/> element when calling ObjectInputStream.readFields()");
                }
                reader.moveUp();
                return fields;
            }

            @Override
            public void defaultReadObject(){
                if (serializationMembers.getSerializablePersistentFields(currentType[0]) != null){
                    readFieldsFromStream();
                    return;
                }
                if (!reader.hasMoreChildren()){
                    return;
                }
                reader.moveDown();
                if (!reader.getNodeName().equals(ELEMENT_DEFAULT)){
                    throw new ConversionException("Expected <" + ELEMENT_DEFAULT + "/> element in readObject() stream");
                }
                while (reader.hasMoreChildren()){
                    reader.moveDown();

                    String fieldName = mapper.realMember(currentType[0], reader.getNodeName());
                    if (mapper.shouldSerializeMember(currentType[0], fieldName)){
                        String classAttribute = HierarchicalStreams.readClassAttribute(reader, mapper);
                        final Class type;
                        if (classAttribute != null){
                            type = mapper.realClass(classAttribute);
                        }else{
                            type = mapper.defaultImplementationOf(reflectionProvider.getFieldType(result, fieldName, currentType[0]));
                        }

                        Object value = context.convertAnother(result, type);
                        reflectionProvider.writeField(result, fieldName, value, currentType[0]);
                    }

                    reader.moveUp();
                }
                reader.moveUp();
            }

            @Override
            public void registerValidation(final ObjectInputValidation validation,int priority){
                context.addCompletionCallback(() -> {
                    try{
                        validation.validateObject();
                    }catch (InvalidObjectException e){
                        throw new ObjectAccessException("Cannot validate object", e);
                    }
                }, priority);
            }

            @Override
            public void close(){
                throw new UnsupportedOperationException("Objects are not allowed to call ObjectInputStream.close() from readObject()");
            }
        };

        while (reader.hasMoreChildren()){
            reader.moveDown();
            String nodeName = reader.getNodeName();
            if (nodeName.equals(ELEMENT_UNSERIALIZABLE_PARENTS)){
                super.doUnmarshal(result, reader, context);
            }else{
                String classAttribute = HierarchicalStreams.readClassAttribute(reader, mapper);
                if (classAttribute == null){
                    currentType[0] = mapper.defaultImplementationOf(mapper.realClass(nodeName));
                }else{
                    currentType[0] = mapper.realClass(classAttribute);
                }
                if (serializationMembers.supportsReadObject(currentType[0], false)){
                    CustomObjectInputStream objectInputStream = CustomObjectInputStream
                                    .getInstance(context, callback, classLoaderReference);
                    serializationMembers.callReadObject(currentType[0], result, objectInputStream);
                    objectInputStream.popCallback();
                }else{
                    try{
                        callback.defaultReadObject();
                    }catch (IOException e){
                        throw new StreamException("Cannot read defaults", e);
                    }
                }
            }
            reader.moveUp();
        }

        return result;
    }

    protected void doMarshalConditionally(final Object source,final HierarchicalStreamWriter writer,final MarshallingContext context){
        if (isSerializable(source.getClass())){
            doMarshal(source, writer, context);
        }else{
            super.doMarshal(source, writer, context);
        }
    }

    protected Object doUnmarshalConditionally(final Object result,final HierarchicalStreamReader reader,final UnmarshallingContext context){
        return isSerializable(result.getClass()) ? doUnmarshal(result, reader, context) : super.doUnmarshal(result, reader, context);
    }

    private static class UnserializableParentsReflectionProvider extends ReflectionProviderWrapper{

        public UnserializableParentsReflectionProvider(final ReflectionProvider reflectionProvider){
            super(reflectionProvider);
        }

        @Override
        public void visitSerializableFields(final Object object,final Visitor visitor){
            wrapped.visitSerializableFields(object, (name,type,definedIn,value) -> {
                if (!Serializable.class.isAssignableFrom(definedIn)){
                    visitor.visit(name, type, definedIn, value);
                }
            });
        }
    }
}
